返回文章列表

Redis Cluster 分片与高可用

系统梳理 Redis Cluster 的 Hash Slot、Hash Tag、MOVED、ASK、Cluster Bus、Gossip、PFAIL、FAIL、副本选举、槽位迁移、同槽约束与 Go 客户端实践。

第 17 章:Redis Cluster 分片与高可用

1. 本章定位

Redis Sentinel 解决的是单个主从复制组的自动故障转移问题,但数据仍主要由一个主节点承载。Redis Cluster 在主从复制和自动故障转移的基础上,引入了数据分片,使多个主节点可以同时承载不同的数据和请求。

理解 Redis Cluster,可以先建立下面这条主线:

Key → Hash Slot → Master → Replica

客户端先根据 Key 计算 Hash Slot,再根据槽位拓扑找到负责该槽的主节点。主节点负责读写,副本异步复制数据,并在主节点故障后参与选举和接管槽位。

Redis Cluster 的核心目标是横向扩展、高吞吐和一定程度的故障可用性,而不是跨分片事务或强一致性。它没有代理层,正常情况下客户端直接访问目标节点;多 Key 命令、事务和 Lua 脚本只有在相关 Key 属于同一槽时才能执行。Redis Cluster 仅支持数据库 0。(Redis)


2. 学习目标

学完本章后,应能够:

  1. 根据普通 Key 和带 Hash Tag 的 Key 判断其 Hash Slot。
  2. 解释 Redis Cluster 为什么使用固定的 16384 个槽,而不是经典一致性哈希环。
  3. 准确区分 MOVEDASK 重定向,并说明客户端应该如何处理。
  4. 解释 Cluster Bus、Gossip、PFAILFAIL 和副本选举之间的关系。
  5. 分析多数主节点不可达、槽位失去覆盖、主从同时故障时的集群行为。
  6. 解释为什么 Redis Cluster 已确认的写入仍可能丢失,以及 WAIT 能改善什么、不能保证什么。
  7. 设计满足同槽约束的 Key,并使用 go-redis 正确执行事务、Lua 和批量请求。
  8. 制定扩容、缩容、槽位迁移和热点槽治理方案。

2.1 本章边界与跳转

本章是 Redis Cluster 分片与分片级高可用的主章节。Sentinel 只解决单个主从复制组的自动故障转移,见[第 16 章](/blog/tech/Redis/16.Sentinel 高可用/);复制确认和数据丢失窗口见第 15 章;多 Key、事务、Lua 和 Pipeline 的同槽约束在[第 9 章](/blog/tech/Redis/09.Pipeline、事务、WATCH、Lua 与 Functions/)继续展开;容量规划、热 Slot 和水平分片的综合面试回答见第 20 章

3. 核心概念

3.1 Redis Cluster 的目标与拓扑

Redis Cluster 主要解决两个问题:

  • 数据容量和吞吐横向扩展:不同主节点持有不同槽位,多个主节点可以并行处理请求。
  • 分片级高可用:每个主节点可以配置一个或多个副本,主节点故障后由副本接管。

一个典型的六节点集群如下:

Shard A: Master A ← Replica A1
Shard B: Master B ← Replica B1
Shard C: Master C ← Replica C1

Redis 7.0 之后,官方通过 CLUSTER SHARDS 明确定义了 Shard:一组服务相同槽位、互相复制的节点,其中同一时间最多有一个主节点,可以有零个或多个副本。(Redis)

Redis Cluster 不在节点之间代理客户端命令。客户端即使连接到了错误节点,也会收到 MOVEDASK,随后访问真正负责该槽位的节点。因此,生产应用必须使用 Cluster-Aware Client,而不是把普通单机客户端随意连接到某个集群节点。(Redis)


3.2 16384 个 Hash Slot

Redis Cluster 将整个 Key 空间划分为 16384 个槽,编号范围为:

0 ~ 16383

普通 Key 的槽位计算公式为:

slot = CRC16(key) mod 16384

由于 16384 等于 2^14,实现上也可以理解为使用 CRC16 结果的低 14 位:

slot = CRC16(key) & 0x3FFF

计算 CRC16 的时间复杂度是 O(L),其中 L 是参与计算的 Key 或 Hash Tag 字节数;计算出槽号后,通过槽位表查找目标节点可以近似视为 O(1)。(Redis)

为什么是 16384,而不是 65536?

面试中不宜只背“因为心跳包太大”这一句话。更完整的解释是:

  1. 槽位必须足够多,才能提供较细的迁移和负载均衡粒度。
  2. 槽位也不能无限多,因为节点需要传播自身负责槽位的位图。
  3. 16384 个槽对应 16384 位,即原始位图约为 2048 字节;若使用 65536 个槽,位图会扩大到约 8192 字节。
  4. 16384 已经能支持很大的主节点数量,实际集群通常远小于这个理论上限。
  5. 固定槽位数使服务端和客户端都可以维护结构简单、查询高效的槽位映射表。

Cluster 心跳消息中确实包含节点负责槽位的位图,因此槽数会直接影响集群元数据传播成本。(Redis)


3.3 为什么不使用经典一致性哈希

Redis Cluster 官方明确说明它不使用一致性哈希,而是采用固定 Hash Slot。(Redis)

两种模型的主要差异如下:

维度经典一致性哈希Redis Cluster Hash Slot
Key 定位Key 映射到哈希环上的节点或虚拟节点Key 先映射到固定槽,再由槽映射到节点
节点变化环结构变化,部分 Key 重新映射显式移动部分槽位
客户端元数据节点环、虚拟节点列表16384 槽到节点的映射
迁移控制通常由环变化决定范围运维可以精确选择迁移哪些槽
多 Key 共置需要额外的路由设计原生 Hash Tag
运维代价依赖具体实现槽位迁移需要复制实际 Key

Hash Slot 增加了一层间接映射:

key → slot → node

这使扩容时不需要改变 key → slot 的结果,只需要改变 slot → node 的归属。客户端可以缓存一个固定范围的槽位表,故障转移和迁槽也可以通过更新槽位所有权传播。

一致性哈希并不是“错误方案”。它更适合客户端分片、缓存节点环和允许节点增减时自动重映射的场景。Redis Cluster 选择槽位,是为了获得更明确的槽位所有权、可控迁移和统一的客户端协议。


3.4 Hash Tag

Redis Cluster 要求多 Key 命令涉及的所有 Key 属于同一槽。Hash Tag 可以强制多个 Key 使用同一段内容计算槽位:

user:{42}:profile
user:{42}:quota
user:{42}:version

这三个 Key 都只对 42 计算 CRC16,因此属于同一个槽。

Hash Tag 的匹配规则不是“取任意一对大括号”,而是寻找第一对有效的 {...}

  • 必须存在 {
  • 其右侧必须存在 }
  • 两者之间至少有一个字符。
  • 只使用第一对有效括号中的内容。

例如:

foo{bar}{other}  → 使用 bar
foo{}bar         → 对整个 Key 计算
foo{{bar}}       → 使用 {bar

官方规范给出了完整的 Hash Tag 解析规则。Redis 8.0 还针对能够确定为单槽的 KEYSSCANSORT 模式增加了单槽扫描优化。(Redis)

Hash Tag 的代价

Hash Tag 解决了数据共置问题,但也可能破坏分片效果:

{all-users}:profile:1
{all-users}:profile:2
{all-users}:profile:3

以上所有 Key 都进入同一槽,相当于主动放弃水平分片。

更合理的原则是:

  • 按需要原子操作的最小业务实体打 Tag。
  • 用户维度操作可使用 {userID}
  • 订单维度操作可使用 {orderID}
  • 不要直接使用全局常量或超大租户 ID 聚合全部数据。
  • 如果业务 ID 可能包含 {},应先进行稳定编码,避免意外改变 Tag。

3.5 MOVED 与 ASK

对比项MOVEDASK
含义槽位已经由另一个节点负责槽位正在迁移,本次请求暂时去另一个节点
持续性通常是长期或永久映射变化仅对当前被重定向的命令有效
客户端槽表应更新或重新加载不应永久修改
后续请求直接访问新节点仍先访问原槽位节点
附加命令向目标节点先发送 ASKING
常见场景故障转移完成、迁槽完成、槽表过期经典在线迁槽过程中

MOVED

典型响应:

MOVED 12182 10.0.0.12:6379

表示槽 12182 现在由 10.0.0.12:6379 负责。

客户端可以立即修正该槽映射,但成熟客户端通常还会异步重新加载完整拓扑,以免连续遭遇大量 MOVED

ASK

典型响应:

ASK 12182 10.0.0.12:6379

客户端应在同一条目标连接上依次执行:

ASKING
原始命令

ASKING 设置的是一次性连接状态,只允许下一条命令临时访问正在导入的槽。客户端不能因为一次 ASK 就永久修改槽表,否则后续尚未迁走的 Key 可能被错误地发送到目标节点。(Redis)


3.6 Cluster-Aware Client

一个成熟的集群客户端通常执行以下流程:

  1. 使用多个 Seed 地址连接任一可用节点。
  2. 读取集群拓扑和槽位分配。
  3. 在本地缓存 slot → node 映射。
  4. 根据命令涉及的 Key 计算槽位。
  5. 从目标节点的连接池取得连接并执行命令。
  6. 遇到 MOVED 时更新或刷新拓扑。
  7. 遇到 ASK 时向目标节点发送 ASKING + 原命令
  8. 遇到网络错误、故障转移或拓扑抖动时进行有限重试和退避。

Redis 7.0+ 提供更具扩展性的 CLUSTER SHARDS,旧客户端也可能使用 CLUSTER SLOTSCLUSTER SHARDS 适合客户端在启动时建立拓扑,并在收到 MOVED 后重新加载。(Redis)

Cluster 客户端必须能访问拓扑中返回的每一个节点地址。只让客户端访问一个入口地址,而节点返回的 IP、端口不可达,会导致客户端收到 MOVED 后连接失败。

原生 Redis Cluster 对 NAT、端口重映射和错误的节点公告地址比较敏感。工程上的本质要求是:

  • 客户端端口对应用可达。
  • Cluster Bus 端口在所有节点之间可达。
  • 节点公告的 IP、主机名和端口必须与实际网络一致。(Redis)

3.7 Cluster Bus、Gossip、PFAIL 与 FAIL

每个 Redis Cluster 节点通常有两个端口:

  • 客户端命令端口:处理 GETSET 等命令。
  • Cluster Bus 端口:节点之间进行故障检测、拓扑传播、选举授权和配置更新。

Cluster Bus 使用二进制协议,客户端不应连接该端口。(Redis)

心跳消息包含:

  • Node ID。
  • currentEpochconfigEpoch
  • 节点角色和状态。
  • 节点负责槽位的位图。
  • 客户端端口和 Cluster Bus 端口。
  • 发送方观察到的集群状态。
  • 少量随机节点的 Gossip 信息。

Gossip 不是每次广播所有节点的完整状态,而是抽取一部分节点信息随心跳传播,在控制消息大小的同时实现最终扩散。(Redis)

PFAIL

PFAIL 表示 Possible Failure,是某个节点的本地怀疑

当节点向另一个节点发送 Ping,超过 cluster-node-timeout 仍未收到有效响应时,就可能将对方标记为 PFAIL

单个节点的网络抖动、CPU 卡顿或连接异常,不应立即触发主从切换,因此 PFAIL 本身不会直接启动故障转移。

FAIL

FAIL 表示故障已经得到集群层面的确认。

当某个节点收集到多数主节点在有效时间窗口内对目标节点的 PFAILFAIL 报告后,可以将其提升为 FAIL,并广播专门的 FAIL 消息。(Redis)

这个过程使用了一种较弱的多数派确认,但它不是 Raft 那样对每条业务写入进行共识复制。这里达成的是“某节点是否故障”的判断,而不是业务数据日志的一致提交。


3.8 主从复制与故障转移

当主节点被确认进入 FAIL 状态后,其副本可以参与选举。

大致流程如下:

  1. 主节点被标记为 FAIL
  2. 副本检查自己是否足够新、主从断开时间是否在允许范围内。
  3. 多个副本根据复制偏移量计算 Rank。
  4. 数据更新程度较高的副本通常更早发起选举。
  5. 候选副本增加 currentEpoch,向主节点请求投票。
  6. 获得多数主节点投票后,副本晋升为主节点。
  7. 新主节点获得更高的 configEpoch
  8. 新主节点通过心跳宣布自己接管原主节点的槽位。
  9. 客户端通过 MOVED 或拓扑刷新逐渐切换到新主节点。
  10. 旧主节点恢复并发现更高 Epoch 后,通常会降级为新主节点的副本。

副本选举需要多数主节点授权;复制偏移量更靠前的副本会获得较低 Rank,从而更早尝试选举。最终由更高 configEpoch 的槽位声明胜出。(Redis)

因此,故障恢复时间不应简单等同于 cluster-node-timeout。真实恢复时间还包含:

故障发现
+ PFAIL/FAIL 传播
+ 副本选举延迟
+ 拓扑传播
+ 客户端重连和重试

3.9 多数派、可用性与一致性边界

Redis Cluster 能够在以下条件下继续提供主要服务:

  • 多数主节点仍可互相通信。
  • 每个不可达的主节点都有可用且具备选举资格的副本。
  • 槽位能够被新的主节点继续覆盖。

当多数主节点不可达时,集群无法获得故障转移所需多数票,通常会进入失败状态。官方文档明确指出,较大故障,例如多数主节点不可用,会导致集群不可用。(Redis)

默认配置还包括:

  • cluster-require-full-coverage yes:部分槽无人负责时,集群停止接受写入。
  • cluster-allow-reads-when-down no:集群处于失败状态时,默认停止提供流量。

可以调整这些配置以允许部分槽继续服务或在失败状态下读取,但这会改变可用性和一致性边界,不能把它理解为恢复了安全的多数派写入能力。(Redis)

Redis Cluster 不是强一致系统

主节点默认使用异步复制:

客户端 → 主节点写入
主节点 → 向客户端返回 OK
主节点 → 异步传播给副本

如果主节点返回 OK 后立即故障,而该写入尚未到达副本,随后一个旧副本被提升为主节点,这次已经确认的写入就会丢失。(Redis)

网络分区时风险更明显:

  • 客户端与少数派主节点处在一起。
  • 少数派主节点可能在一段时间内继续接收写入。
  • 多数派将其副本提升为新主节点。
  • 分区恢复后,旧主节点的数据被新主节点覆盖。
  • 少数派窗口内的写入丢失。

少数派主节点在超过 cluster-node-timeout、无法感知多数主节点后会停止接受写入,因此风险窗口有上限,但并不为零。(Redis)

WAIT 可以要求写入在返回前到达一定数量的副本,从而降低数据丢失概率,但即使使用 WAIT,Redis Cluster 仍不提供严格强一致性:复杂故障条件下,未收到该写入的副本仍可能当选。(Redis)


3.10 多 Key、事务与 Lua 的同槽限制

以下操作通常要求所有 Key 属于同一槽:

  • MGETMSET
  • 集合交集、并集、差集等多 Key 命令。
  • MULTI/EXEC 事务中的所有带 Key 命令。
  • WATCH 涉及的 Key。
  • Lua 脚本访问的所有 Key。
  • Redis Functions 中需要共同执行的 Key。

例如:

MGET user:{42}:profile user:{42}:quota

可以执行,因为两个 Key 同槽。

而:

MGET user:42:profile user:99:profile

通常返回:

CROSSSLOT Keys in request don't hash to the same slot

Redis Cluster 不提供跨分片的原子提交协议。即使客户端可以把多条命令分别发到不同分片,也只是客户端聚合结果,不是分布式事务。(Redis)

需要特别区分:

  • 多 Key 命令:一条服务端命令,必须同槽。
  • 事务或 Lua:要求原子执行,因此必须同槽。
  • 普通 Pipeline:只是批量发送和减少网络往返,客户端可以按节点拆分,但不提供跨节点原子性。

3.11 在线扩缩容、槽位迁移与热点槽

经典槽位迁移

传统迁槽的核心状态为:

源节点:MIGRATING
目标节点:IMPORTING

流程通常是:

  1. 将目标节点上的槽设置为 IMPORTING
  2. 将源节点上的槽设置为 MIGRATING
  3. 使用 CLUSTER GETKEYSINSLOT 找出槽内 Key。
  4. 使用 MIGRATE 将 Key 从源节点复制到目标节点。
  5. 重复直到该槽没有剩余 Key。
  6. 使用 CLUSTER SETSLOT ... NODE ... 正式切换槽位归属。
  7. 将新槽位所有权传播给集群其他节点。

迁移期间:

  • 源节点上的已有 Key 仍由源节点处理。
  • 如果源节点发现请求的 Key 已经迁走,会返回 ASK
  • 目标节点只有在客户端先发送 ASKING 时,才临时处理正在导入槽的请求。(Redis)

迁移槽的本质是迁移槽内的全部 Key。槽数量相同,不代表迁移成本相同;一个包含 BigKey 的槽,迁移成本可能远大于包含大量小 Key 的槽。官方规范也明确提醒,在低延迟要求下迁移 BigKey 并不明智。(Redis)

Redis 8.4+ 原子槽迁移

Redis Open Source 8.4 增加了 CLUSTER MIGRATION,可在目标主节点发起槽范围导入,并查询或取消迁移任务:

CLUSTER MIGRATION IMPORT 0 1000
CLUSTER MIGRATION STATUS ALL

这里的“原子”主要强调最终槽位所有权的交接,而不是所有数据瞬间完成复制。后台仍需要复制槽数据,并在最终交接时协调写入暂停和复制追平。

迁移过程中,未拥有槽、正在导入或裁剪的 Key 可能暂时不出现在 SCANDBSIZECLUSTER GETKEYSINSLOT 等结果中,INFO KEYSPACE 仍会反映实际 Key 数量。(Redis)

扩容

增加空主节点
→ 加入集群
→ 从现有主节点迁入一部分槽
→ 检查槽覆盖与负载
→ 为新主节点补充副本

缩容

把目标主节点的全部槽迁到其他主节点
→ 确认节点不再负责任何槽
→ 删除节点

主节点必须先清空槽位,才能安全从集群移除。(Redis)

热点槽与数据倾斜

槽数量平均不代表负载平均。常见原因包括:

  • Hash Tag 粒度过粗。
  • 单个 HotKey 请求量极高。
  • 单个 BigKey 占用大量内存或网络。
  • 不同槽内 Key 数量相近,但 Value 大小差异巨大。
  • 时间型 Key 或顺序型业务集中访问近期数据。
  • 少数大租户远大于其他租户。
  • 全局排行榜、全局计数器等单 Key 结构天然只能位于一个槽。

Redis 8.2+ 提供 CLUSTER SLOT-STATS,可以按槽查看 Key 数、CPU、内存和网络指标。其中除 KEY-COUNT 外的指标需要开启 cluster-slot-stats-enabled。该命令只返回当前 Shard 的槽统计,分析整个集群时要查询所有主节点。(Redis)


4. 命令与 Go 使用方法

4.1 redis-cli 操作

连接 Cluster

redis-cli -c -h 127.0.0.1 -p 7000

-c 表示自动跟随 MOVEDASK 重定向。

官方示例说明,redis-cli -c 会根据节点返回的重定向访问目标节点,而成熟客户端通常还会缓存完整槽位映射。(Redis)

查看集群状态

CLUSTER INFO
CLUSTER NODES
CLUSTER SHARDS

Redis 7.0 之前或需要兼容旧客户端时,可查看:

CLUSTER SLOTS

计算槽位

CLUSTER KEYSLOT user:{42}:profile
CLUSTER KEYSLOT user:{42}:quota
CLUSTER KEYSLOT user:{99}:profile

前两个结果相同,第三个通常不同。

验证同槽命令

MSET user:{42}:profile '{"name":"alice"}' user:{42}:quota 100
MGET user:{42}:profile user:{42}:quota

跨槽示例:

MGET user:{42}:profile user:{99}:profile

预期返回:

(error) CROSSSLOT Keys in request don't hash to the same slot

观察 MOVED

不使用 -c 连接:

redis-cli -h 127.0.0.1 -p 7000 GET user:{42}:profile

可能返回:

(error) MOVED 8000 127.0.0.1:7001

手工处理 ASK

迁槽期间收到:

ASK 8000 127.0.0.1:7002

连接目标节点,并在同一连接上执行:

ASKING
GET user:{42}:profile

添加主节点

redis-cli --cluster add-node \
  127.0.0.1:7006 \
  127.0.0.1:7000

新主节点加入后最初没有槽,也不会自动分担已有数据,需要继续执行迁槽或 Rebalance。(Redis)

重新分配槽位

交互式:

redis-cli --cluster reshard 127.0.0.1:7000

自动化示意:

redis-cli --cluster reshard 127.0.0.1:7000 \
  --cluster-from <source-node-id> \
  --cluster-to <target-node-id> \
  --cluster-slots 1000 \
  --cluster-yes

执行完成后检查:

redis-cli --cluster check 127.0.0.1:7000

官方 redis-cli --cluster reshard 会发现其余节点并迁移指定槽位。该过程可以在线执行,但“在线”不等于完全没有延迟波动。(Redis)

删除节点

redis-cli --cluster del-node \
  127.0.0.1:7000 \
  <node-id>

删除主节点前必须迁走其全部槽位。

查看热点槽

Redis 8.2+:

CLUSTER SLOT-STATS ORDERBY CPU-USEC LIMIT 10 DESC
CLUSTER SLOT-STATS ORDERBY MEMORY-BYTES LIMIT 10 DESC
CLUSTER SLOT-STATS ORDERBY NETWORK-BYTES-OUT LIMIT 10 DESC

Redis 8.4+ 原子迁槽

在目标主节点执行:

CLUSTER MIGRATION IMPORT 0 1000
CLUSTER MIGRATION STATUS ALL

取消任务:

CLUSTER MIGRATION CANCEL ID <task-id>

这是 @admin@slow@dangerous 命令,生产执行前必须确认版本、备份、容量、复制延迟和回滚路径。(Redis)


4.2 Go:创建 ClusterClient

官方 Go 客户端通过 redis.NewClusterClient 创建集群客户端,并接收一个或多个 Seed 地址。(Redis)

package rediscluster

import (
	"context"
	"errors"
	"fmt"
	"time"

	"github.com/redis/go-redis/v9"
)

func NewClient(addrs []string) *redis.ClusterClient {
	return redis.NewClusterClient(&redis.ClusterOptions{
		Addrs: addrs,

		Username: "app",
		Password: "replace-with-secret",

		// MOVED、ASK 和部分网络错误的最大重定向次数。
		MaxRedirects: 3,

		DialTimeout:  2 * time.Second,
		ReadTimeout:  800 * time.Millisecond,
		WriteTimeout: 800 * time.Millisecond,
		PoolTimeout:  1 * time.Second,

		// 注意:ClusterClient 中 PoolSize 是每个节点的连接池大小。
		PoolSize: 32,

		// 让命令 Context 的截止时间参与超时控制。
		ContextTimeoutEnabled: true,
	})
}

// Ping 只能作为基本连通性检查,不能证明所有 Shard 都健康。
func Ping(parent context.Context, rdb *redis.ClusterClient) error {
	ctx, cancel := context.WithTimeout(parent, 2*time.Second)
	defer cancel()

	if err := rdb.Ping(ctx).Err(); err != nil {
		return fmt.Errorf("ping redis cluster: %w", err)
	}
	return nil
}

func GetUserProfile(
	parent context.Context,
	rdb *redis.ClusterClient,
	userID string,
) (value string, found bool, err error) {
	ctx, cancel := context.WithTimeout(parent, 1200*time.Millisecond)
	defer cancel()

	key := fmt.Sprintf("user:{%s}:profile", userID)

	value, err = rdb.Get(ctx, key).Result()
	switch {
	case err == nil:
		return value, true, nil
	case errors.Is(err, redis.Nil):
		return "", false, nil
	default:
		return "", false, fmt.Errorf("get %q: %w", key, err)
	}
}

工程注意事项:

  • ClusterClient 可以被多个 Goroutine 并发复用,不要为每个请求创建新客户端。
  • 客户端应在进程启动时创建,在进程退出时统一 Close
  • PoolSizeMaxActiveConns每个集群节点的配置,不是整个集群的总连接数。
  • MaxRedirects 默认用于处理网络错误及 MOVEDASK,不宜无限增大,否则故障期间可能形成重试风暴。
  • RouteByLatencyRouteRandomly 会启用副本读取,只适用于能够接受旧数据的场景。(GitHub)

4.3 Go:通过 Hash Tag 保证事务同槽

func UpdateUserAtomically(
	parent context.Context,
	rdb *redis.ClusterClient,
	userID string,
	name string,
) error {
	ctx, cancel := context.WithTimeout(parent, 1500*time.Millisecond)
	defer cancel()

	profileKey := fmt.Sprintf("user:{%s}:profile", userID)
	versionKey := fmt.Sprintf("user:{%s}:version", userID)

	_, err := rdb.TxPipelined(ctx, func(pipe redis.Pipeliner) error {
		pipe.HSet(ctx, profileKey, "name", name)
		pipe.Incr(ctx, versionKey)
		return nil
	})
	if err != nil {
		return fmt.Errorf("update user %s atomically: %w", userID, err)
	}
	return nil
}

TxPipelined 会使用 MULTI/EXEC,因此这里的两条命令在目标 Shard 上原子执行。但它并没有产生跨分片事务能力;如果两个 Key 不同槽,当前 go-redis ClusterClient 会返回跨槽错误。


4.4 Go:跨槽普通 Pipeline

普通 Pipeline 中的命令可以属于不同槽。当前 go-redis ClusterClient 会根据目标节点对命令分组,并并发发送到不同节点,但这不是一个原子操作。(GitHub)

func BatchSetIndependentUsers(
	parent context.Context,
	rdb *redis.ClusterClient,
) error {
	ctx, cancel := context.WithTimeout(parent, 1500*time.Millisecond)
	defer cancel()

	pipe := rdb.Pipeline()

	cmdA := pipe.Set(
		ctx,
		"user:{42}:last_seen",
		time.Now().Unix(),
		24*time.Hour,
	)
	cmdB := pipe.Set(
		ctx,
		"user:{99}:last_seen",
		time.Now().Unix(),
		24*time.Hour,
	)

	_, execErr := pipe.Exec(ctx)
	if execErr == nil {
		return nil
	}

	var errs []error
	errs = append(errs, fmt.Errorf("pipeline exec: %w", execErr))

	if err := cmdA.Err(); err != nil {
		errs = append(errs, fmt.Errorf("user 42: %w", err))
	}
	if err := cmdB.Err(); err != nil {
		errs = append(errs, fmt.Errorf("user 99: %w", err))
	}

	return errors.Join(errs...)
}

必须考虑以下结果:

Shard A 命令成功
Shard B 命令失败
Pipeline 整体返回错误

此时不能不加区分地重放整个 Pipeline。对于 SET 这类幂等命令,重试通常较容易控制;对于 INCRLPUSH、扣减库存等非幂等操作,网络错误可能发生在“服务端已经执行、客户端没有收到响应”之后,盲目重试可能造成重复执行。


5. 典型业务场景

业务场景适合度数据与一致性特点主要风险可选替代方案
海量用户 Session、Token用户之间基本独立,允许按用户分片热门账号、Value 过大、TTL 集中本地缓存加 Redis、专用会话服务
用户维度限流每个用户独立;多个限流 Key 可使用 {userID}大租户热点、过度聚合 Tag网关本地限流、专用限流系统
购物车、用户配置较高同一用户相关 Key 可同槽单用户数据过大、跨用户操作受限Hash 单 Key、关系数据库
分租户缓存条件适合租户之间可分片超大租户形成热点槽大租户二级分片、独立集群
全局排行榜单个 ZSet较低单个 Key 只能位于一个槽单分片 CPU、内存和网络瓶颈分区排行榜后合并、分析数据库
全局精确计数器较低单 Key 热点,无法自动拆分热 Key、主节点瓶颈分片计数后汇总、流处理系统
严格资金账本不适合充当权威存储要求强一致、审计和事务已确认写入仍可能丢失关系数据库、共识型数据库
跨租户原子事务不适合Key 天然跨槽无跨分片事务数据库事务、消息/Saga
大规模缓存集群可接受短暂失败和缓存重建雪崩、重试风暴、热槽Cluster 配合本地缓存和降级

Redis Cluster 最适合“数据可以自然按业务实体分区、单实体操作能够局限在一个槽、可接受异步复制一致性”的场景。


6. 底层实现

6.1 核心数据结构

概念上,一个 Cluster 节点需要维护:

  1. 节点表 保存已知节点的 Node ID、地址、角色、主从关系、存活状态和 Epoch。

  2. 16384 槽所有权表 每个槽指向当前负责它的主节点,便于 O(1) 定位槽所有者。

  3. 节点槽位位图 表示某节点负责哪些槽,用于心跳和拓扑传播。

  4. 主从复制状态 包括复制偏移量、主从连接状态和副本新鲜度。

  5. 槽内 Key 索引 除主 Keyspace 字典外,还需要能够快速查找某个槽中的 Key,以支持 CLUSTER GETKEYSINSLOT 和迁槽。

  6. Epoch 信息 currentEpoch 用于选举轮次,configEpoch 用于判断不同槽位所有权声明的新旧。

当两个节点同时声称拥有同一槽时,更高 configEpoch 的声明获胜。旧节点恢复后会通过心跳或 UPDATE 消息学习最新槽位所有权。(Redis)


6.2 正常命令执行路径

客户端解析命令 Key
→ 计算 CRC16 和槽位
→ 查本地槽位映射
→ 从目标节点连接池获取连接
→ 网络发送命令
→ 目标主节点执行命令
→ 返回结果

时间主要由以下部分组成:

Key 哈希计算
+ 客户端路由
+ 连接池等待
+ 网络 RTT
+ Redis 命令执行
+ 响应传输

Cluster 不会让一次普通 GET 同时访问多个主节点。分片提高的是整个集群的聚合吞吐和容量,不会自动降低单条命令的网络延迟。


6.3 重定向路径

MOVED

客户端槽表过期
→ 请求到旧节点
→ 收到 MOVED
→ 访问新节点
→ 更新或重新加载拓扑

相较正常路径,至少增加一次错误响应和一次额外网络访问。

ASK

请求到迁出节点
→ Key 已迁走
→ 收到 ASK
→ 连接迁入节点
→ ASKING
→ 原始命令

ASK 还可能增加连接切换和两条命令发送成本,因此大规模迁槽期间,尾延迟通常会高于稳定状态。


6.4 故障检测和选举不是业务命令共识

Cluster Bus 后台持续执行:

Ping/Pong
→ Gossip 状态传播
→ PFAIL
→ 多数主节点故障报告
→ FAIL
→ 副本选举
→ 新 configEpoch
→ 槽位所有权传播

这些是后台控制面任务,不是每条业务命令的同步提交阶段。因此:

  • 正常写入不等待多数主节点确认。
  • 正常写入也不等待副本持久化后再返回。
  • 故障检测的多数派不能等同于业务数据的强一致多数派复制。

这正是 Redis Cluster 能保持较低写延迟,但存在非零 RPO 的原因。


6.5 迁槽成本分析

操作命令本身网络与数据成本后台或阻塞影响
CLUSTER SETSLOT修改槽状态,成本较低传播少量元数据等待 Gossip 或主动通知
CLUSTER GETKEYSINSLOT查找槽内部分 Key返回 Key 名称批次过大增加响应体
MIGRATE序列化和删除 Key传输完整 Key 与 ValueBigKey 可造成明显延迟
redis-cli --cluster reshard编排多个命令传输所有待迁 Key迁移越久,ASK 窗口越长
CLUSTER MIGRATION发起任务的命令按槽范围处理后台复制槽数据最终交接可能短暂停写

因此,不能只根据“迁移 1000 个槽”估算时间。更关键的指标是:

  • 槽内 Key 数量。
  • 数据总字节数。
  • BigKey 分布。
  • 源节点和目标节点 CPU。
  • 主从复制延迟。
  • 网络带宽。
  • 迁移期间写入速率。

6.6 为什么使用这些结构

固定槽位表

优点:

  • 路由简单。
  • 客户端容易缓存。
  • 故障转移只需改变槽所有者。
  • 运维可以精确选择迁移范围。

代价:

  • 必须管理槽迁移状态。
  • 负载均衡粒度受槽内数据分布影响。
  • 槽均匀不代表 QPS、内存均匀。

位图

优点:

  • 表达槽集合紧凑。
  • 可随心跳传播。
  • 集合更新和判断高效。

代价:

  • 槽数越多,心跳固定负担越大。

Gossip

优点:

  • 无单独中心协调节点。
  • 节点能够逐步发现拓扑和故障。
  • 单次消息不必携带所有节点完整状态。

代价:

  • 状态传播存在延迟。
  • 网络抖动时不同节点可能短暂持有不同观察。
  • 节点越多、NODE_TIMEOUT 越小,控制面消息越多。(Redis)

异步复制

优点:

  • 主节点写入延迟较低。
  • 不需要每条写入等待副本。
  • 聚合吞吐较高。

代价:

  • 已确认写入仍有丢失窗口。
  • 副本读取可能过期。
  • 故障转移具有非零 RPO。

6.7 版本差异

能力版本说明
Redis Cluster 基础能力Redis 3.0+固定槽、重定向、主从故障转移
CLUSTER SHARDSRedis 7.0+更可扩展的 Shard 拓扑描述,替代 CLUSTER SLOTS
单槽 Glob 模式优化Redis 8.0+特定 Hash Tag 模式的 SCAN 等只扫描目标槽
CLUSTER SLOT-STATSRedis 8.2+按槽观察 Key、CPU、内存和网络
CLUSTER MIGRATIONRedis 8.4+原子槽迁移任务、状态查询与取消

升级前应确认服务端版本、客户端版本及托管 Redis 产品是否支持相应命令,不能仅根据本地 redis-cli 版本判断。


7. 高性能、高并发、高可用分析

7.1 高性能

Redis Cluster 的性能收益主要来自多个主节点并行:

Master A 处理槽 0~5000
Master B 处理槽 5001~10000
Master C 处理槽 10001~16383

当 Key 分布和请求分布较均匀时,CPU、内存和网络压力可以分散到多个节点。

但要注意:

  • Cluster 提升的是聚合吞吐,不是单命令天然加速。
  • 单个 Key 永远只由一个主节点处理。
  • 一个热门槽仍然只能由一个主节点负责写入。
  • 单个全局 ZSet、Hash 或 List 无法被 Cluster 自动拆分。
  • 频繁 MOVED 说明客户端槽表长期不准确。
  • 迁槽期间的 ASK 会增加额外 RTT。
  • Pipeline 跨节点后,每个节点仍有独立网络往返。

优化重点应包括:

  • 让客户端直接命中正确节点。
  • 控制连接池等待。
  • 合理使用按节点拆分的 Pipeline。
  • 避免过粗 Hash Tag。
  • 在迁槽前治理 BigKey。
  • 同时按槽观察 Key 数、字节数、CPU 和网络,而不是只看槽数量。

7.2 高并发

多个主节点允许不同槽上的命令并行执行,例如:

user:{42}:* → Shard A
user:{99}:* → Shard B
order:{1001}:* → Shard C

但高并发场景容易出现以下问题:

  1. 连接数按节点放大 PoolSize=32、六个节点时,连接数量可能达到数百量级;多个应用实例会进一步放大。

  2. 热点槽打穿单节点 整体集群 CPU 不高,不代表热点主节点没有达到瓶颈。

  3. 故障期间重试放大 客户端同时收到超时、MOVED、连接重置,若每层都重试,可能产生乘法效应。

  4. 跨槽批量部分成功 Pipeline 的一部分节点成功,另一部分失败,业务必须能够识别并补偿。

  5. 拓扑刷新惊群 大量客户端同时感知故障并刷新槽表,可能给集群增加额外控制流量。

建议:

  • 每个业务请求只设置一层明确的重试预算。
  • 使用指数退避和随机抖动。
  • 对非幂等写引入业务幂等 ID。
  • 监控每个节点的连接池等待和活跃连接数。
  • 限制单次 Pipeline 大小,避免大批量响应长时间占用连接。
  • 对热点业务实施应用级二次分片。

7.3 高可用

Redis Cluster 的高可用依赖:

  • 主节点多数派仍然可达。
  • 故障主节点存在合格副本。
  • 主副本分布在不同故障域。
  • Cluster Bus 通信正常。
  • 客户端能够访问新主节点地址。
  • 客户端能够及时刷新槽表。

需要同时评估:

RTO

恢复时间受以下因素影响:

cluster-node-timeout
+ FAIL 传播
+ 副本选举
+ 拓扑传播
+ 客户端重试

cluster-node-timeout 过大,故障发现慢;过小则容易因短暂抖动误判,并增加节点间心跳压力。

RPO

由于异步复制,RPO 不是严格为零。复制延迟越大,主节点故障后可能丢失的数据窗口越大。

故障域

如果主节点和它的副本位于同一台宿主机、同一机架或同一可用区,那么局部基础设施故障可能同时摧毁主副本。

Redis Cluster 的副本迁移机制能够把有多余副本的 Shard 中的一部分副本迁移给没有副本保护的主节点,从而改善后续故障承受能力,但它不能替代正确的初始故障域规划。(Redis)


8. 常见错误与生产事故

事故一:迁移到 Cluster 后大量 CROSSSLOT

现象

原来单机 Redis 中正常运行的 MGET、事务或 Lua 脚本,在 Cluster 中频繁返回 CROSSSLOT

根因

Key 在设计时没有考虑同槽约束,例如:

user:42:profile
quota:42

两个 Key 的业务实体相同,但 Hash Slot 不同。

排查

CLUSTER KEYSLOT user:42:profile
CLUSTER KEYSLOT quota:42

同时梳理事务、Lua、WATCH 和多 Key 命令的 Key 列表。

修复

改为:

user:{42}:profile
user:{42}:quota

旧数据通过双写、回填、灰度读切换完成迁移。

预防

在 Key 设计评审中明确业务原子边界,并为所有多 Key 操作增加槽位测试。


事故二:Hash Tag 过粗导致单节点打满

现象

集群整体 CPU 只有 30%,某个主节点却达到 100%,延迟持续升高。

根因

大量 Key 使用相同 Tag:

cache:{prod}:...

所有生产缓存都进入同一槽。

排查

  • 使用 CLUSTER KEYSLOT 抽样。
  • 查看各主节点 CPU、网络和命令量。
  • Redis 8.2+ 使用 CLUSTER SLOT-STATS
  • 搜索 Key 命名中高度重复的 {...}

修复

按用户、订单、子租户或业务分桶重新设计 Tag。

预防

禁止全局常量 Tag;上线前对真实 Key 样本进行槽分布统计。


事故三:MOVED 指向客户端不可达地址

现象

连接 Seed 节点成功,但执行部分 Key 时超时;日志中先出现 MOVED,随后出现连接拒绝或 DNS 错误。

根因

节点向客户端公告了容器内部 IP、错误端口或不可解析主机名。

排查

  • 直接查看 MOVED 返回地址。
  • 查看 CLUSTER SHARDSCLUSTER SLOTS
  • 从应用网络环境测试所有节点端口。
  • 检查节点公告地址、NAT 和端口映射。

修复

配置客户端可达的节点公告地址和端口,确保数据端口及节点间 Cluster Bus 端口连通。

预防

部署验收时从真实应用网络逐个验证所有节点,而不是只验证入口地址。


事故四:迁槽过程中延迟尖刺

现象

扩容时 P99、P999 延迟明显升高,部分命令超时。

根因

待迁槽中存在 BigKey,MIGRATE 需要序列化并通过网络传输大量数据;同时 ASK 增加额外请求路径。

排查

  • 在迁移前扫描 BigKey。
  • 对比迁移槽的 Key 数和总字节数。
  • 查看源、目标节点 CPU、网络和慢日志。
  • 查看客户端 ASK、超时和重试数量。

修复

暂停或降低迁移速度,先拆分 BigKey,在低峰期按小批次迁移。

预防

建立 BigKey 准入和持续扫描机制;扩容容量预留应早于节点耗尽。


事故五:槽数均衡但内存和 QPS 严重不均

现象

每个主节点负责的槽数大致相等,但部分节点内存、CPU或网络远高于其他节点。

根因

槽数只反映所有权数量,不反映:

  • 槽内 Key 数。
  • Value 字节数。
  • 访问频率。
  • 命令复杂度。
  • 热 Key 分布。

排查

联合分析节点指标、Key 大小、热点 Key、槽级 CPU、内存和网络指标。

修复

按实际负载迁移热点槽;必要时改变业务分片键,而不只是重新平均槽数。

预防

容量模型同时使用槽数、Key 数、字节数、QPS、带宽和命令 CPU。


事故六:故障转移时出现重试风暴和重复写

现象

主节点故障后,应用请求量激增;某些计数、扣减或队列写入发生重复。

根因

  • SDK 自动重试。
  • 业务 RPC 层再次重试。
  • 网关又进行重试。
  • 非幂等命令在响应丢失后被重新执行。
  • 重试没有退避和总预算。

排查

按请求 ID 串联调用链,统计每层重试次数,对比 Redis 实际写入次数。

修复

  • 只保留一层受控重试。
  • 对非幂等操作使用幂等 Token。
  • 使用 Lua 或数据库记录已处理请求。
  • 增加指数退避和随机抖动。

预防

明确区分“命令确定未执行”和“执行结果未知”。网络超时通常属于后者。


事故七:开启副本读取后出现读己之写失败

现象

写入成功后立即读取,有时读到旧值或不存在。

根因

启用了 ReadOnlyRouteByLatencyRouteRandomly,读请求被发送到尚未追平的副本。

排查

检查客户端 ClusterOptions,查看读命令最终命中的节点及复制延迟。

修复

需要 Read-After-Write 的请求固定读取主节点;只有允许陈旧数据的读流量才下放副本。

预防

按业务语义配置客户端,不要为了“读扩展”全局开启副本读取。


事故八:主副本位于同一故障域

现象

一台宿主机或一个可用区故障后,同一 Shard 的主节点和副本同时离线,相关槽无法服务。

根因

只检查了主从数量,没有检查物理部署位置。

排查

建立 Node ID、主从关系、宿主机、机架、可用区的对应表。

修复

重新分配副本,使主副本跨宿主机、机架或可用区。

预防

将故障域分散作为部署和变更的强制校验项。


9. 方案选型与权衡

9.1 部署模式比较

方案水平分片自动故障转移多 Key 限制一致性运维复杂度适用场景
单机 Redis无 Cluster 槽限制单节点语义开发、小数据、可重建缓存
主从复制通常需人工无 Cluster 槽限制异步复制较低读扩展、人工切换
Sentinel无 Cluster 槽限制异步复制单主容量足够,需要自动 HA
Redis Cluster多 Key 必须同槽非强一致大容量、高吞吐、可自然分片
客户端一致性哈希由应用实现由应用决定由各节点决定特殊缓存分片、定制路由
代理或托管分片通常是取决于产品取决于实现取决于产品应用侧较低希望屏蔽拓扑和运维细节
关系/共识数据库通常支持通常支持可支持跨分片事务可提供更强保证较高权威数据、资金、严格事务

选择 Redis Cluster 的前提通常包括:

  • 单主节点容量或吞吐已无法满足要求。
  • 数据能够按用户、订单、设备、租户等自然分区。
  • 主要操作可以局限在单个业务实体。
  • 能接受副本异步复制和非零 RPO。
  • 应用和运维团队能够处理拓扑、迁槽和热点问题。

以下情况不要因为“Redis 很快”而强行选择 Cluster:

  • 强依赖跨分片事务。
  • 权威账本不能接受已确认写入丢失。
  • 主要负载集中在一个全局 Key。
  • 数据访问模式无法形成稳定分片键。
  • 团队无法维护 Cluster-Aware Client 和节点网络拓扑。

9.2 迁槽方式比较

维度经典 MIGRATING/IMPORTINGRedis 8.4+ CLUSTER MIGRATION
兼容性Redis 3.0+ 广泛适用要求 Redis 8.4+
编排需要管理多个槽状态和 MIGRATE目标节点发起迁移任务
请求路由迁移期间常见 ASK支持新的原子交接机制
运维复杂度较高相对集中
数据复制按 Key 迁移后台迁移槽范围
风险BigKey、长 ASK 窗口、状态不一致后台复制压力、交接停写、版本兼容
可观测性依赖 redis-cli 和槽状态提供任务 ID、STATUSCANCEL

即使使用新迁移机制,也必须进行容量评估、限速、低峰执行和失败演练。


10. 高频面试题

问题

1. Redis Cluster 为什么使用 16384 个槽?

推荐回答

30 秒回答: Redis Cluster 先把 Key 映射到固定的 16384 个槽,再把槽分配给节点。固定槽位便于客户端缓存、故障转移和在线迁移。16384 提供了足够的迁移粒度,同时槽位位图只有约 2KB,控制了心跳元数据开销。

展开来说,公式是 CRC16(key) mod 16384。16384 等于 2^14,节点通过槽位位图传播自己负责的槽。槽数太少会导致迁移粒度粗,槽数太多又会增加元数据和运维成本。(Redis)

面试官追问

  1. 为什么不直接把 Key 哈希到节点?
  2. 16384 个槽是否意味着最多只能存 16384 个 Key?
  3. 槽位均匀是否代表负载均匀?

常见错误回答

“因为 Redis 作者随便选了 16384”或“一个槽只能放一个 Key”。

评分点

初级说出公式;中级说明槽是中间层;高级能分析位图、迁移粒度和负载不均问题。


问题

2. Redis Cluster 为什么不用一致性哈希?

推荐回答

30 秒回答: Redis Cluster 使用固定槽位,把 key → slotslot → node 分开。节点增减时只需要显式迁移槽位,不改变 Key 的槽号,客户端也只需维护固定范围的槽表。

一致性哈希适合节点环和客户端分片,但 Redis Cluster 更强调明确的槽位所有权、可控迁移、故障转移和 Hash Tag 共置能力。两者都是分片方案,并非一致性哈希一定更差。(Redis)

面试官追问

  1. 一致性哈希中的虚拟节点解决什么问题?
  2. 扩容时槽位和 Key 分别发生什么变化?
  3. Redis Cluster 扩容是否完全无开销?

常见错误回答

“一致性哈希扩容时会迁移全部数据,所以 Redis 不用。”

评分点

能比较环、虚拟节点、固定槽位和显式迁移,而不是简单否定一致性哈希。


问题

3. Hash Tag 是什么?有什么风险?

推荐回答

30 秒回答: Hash Tag 是 Key 中第一对有效 {...} 之间的内容。Redis 只对这段内容计算槽位,因此多个 Key 可以被强制放在同一槽,满足事务、Lua 和多 Key 命令要求。

风险是 Tag 粒度过粗会把大量数据和流量集中到一个槽。例如所有 Key 都使用 {prod},就会形成单槽热点。(Redis)

面试官追问

  1. foo{}{bar} 使用哪部分计算?
  2. 用户 ID 本身带大括号怎么办?
  3. 同槽是否意味着同一个 Key?

常见错误回答

“只要 Key 中有大括号,Redis 就会取最后一对大括号。”

评分点

掌握第一对有效括号规则、业务原子边界和热点风险。


问题

4. MOVED 和 ASK 有什么区别?

推荐回答

30 秒回答: MOVED 表示槽位归属已经改变,客户端应把后续请求发到新节点并刷新槽表;ASK 表示槽正在迁移,只把当前请求临时发到目标节点,并在同一连接上先发送 ASKING,不能永久修改槽表。

ASK 的存在是因为同一槽中的一部分 Key 可能已经迁走,另一部分仍在源节点。(Redis)

面试官追问

  1. 为什么 ASKING 必须与原命令使用同一连接?
  2. 客户端收到 MOVED 后只修改一个槽够不够?
  3. 哪个错误通常出现在故障转移后?

常见错误回答

“ASK 和 MOVED 都是让客户端以后永远访问新节点。”

评分点

能说清永久与临时、槽表更新和 ASKING 一次性语义。


问题

5. PFAIL 和 FAIL 有什么区别?

推荐回答

30 秒回答: PFAIL 是单个节点在超过 NODE_TIMEOUT 后形成的本地故障怀疑,不足以触发故障转移;FAIL 是收集到多数主节点在有效窗口内的故障报告后形成的集群确认,并会广播给其他节点。

这种设计避免单节点网络抖动直接导致主从切换,但也意味着故障状态传播需要时间。(Redis)

面试官追问

  1. 副本能否把其他节点标记为 PFAIL?
  2. 为什么不能看到一次超时就故障转移?
  3. 这里的多数派是不是业务数据共识?

常见错误回答

“PFAIL 是副本故障,FAIL 是主节点故障。”

评分点

区分本地怀疑和多数派确认,并明确它不是业务写入共识。


问题

6. Redis Cluster 的故障转移过程是什么?

推荐回答

30 秒回答: 主节点先由 PFAIL 升级为 FAIL,符合新鲜度要求的副本根据复制偏移量排序并延迟发起选举。副本增加 Epoch,向主节点请求投票,获得多数票后晋升,取得更高 configEpoch,随后宣布接管原主节点的槽。

旧主恢复后看到更高 Epoch,通常会成为新主的副本。(Redis)

面试官追问

  1. 为什么最新副本优先?
  2. 为什么选举需要多数主节点投票?
  3. RTO 是否等于 cluster-node-timeout

常见错误回答

“主节点断开后,任意副本会立刻自动变主。”

评分点

覆盖 FAIL、候选资格、Rank、投票、Epoch、槽位传播和客户端刷新。


问题

7. 多数主节点不可达时会发生什么?

推荐回答

30 秒回答: Redis Cluster 的自动故障转移需要多数主节点投票。若多数主节点不可达,就无法安全完成选举,集群通常进入失败状态;默认配置下节点会停止提供流量。

如果只是少数主节点故障,并且每个故障主节点都有可用副本,多数派一侧可以完成故障转移。cluster-require-full-coveragecluster-allow-reads-when-down 可以改变部分行为,但不会创造不存在的多数派。(Redis)

面试官追问

  1. 三主三从最多能容忍几个节点故障?
  2. 为什么不能只按节点总数判断?
  3. 主副本同时故障会怎样?

常见错误回答

“只要还有一个 Redis 节点存活,Cluster 就能继续工作。”

评分点

按主节点多数派、Shard 副本覆盖和故障域进行分析,而不是只数存活节点。


问题

8. Redis Cluster 为什么会丢失已经确认的写入?

推荐回答

30 秒回答: 因为主从复制默认是异步的。主节点可以先返回 OK,再把命令传播给副本。如果返回后立即故障,尚未收到该写入的副本被提升为主,写入就会丢失。

网络分区时,少数派旧主在有限窗口内接收的写入,也可能在多数派完成故障转移后被覆盖。WAIT 能降低概率,但不提供严格强一致性。(Redis)

面试官追问

  1. AOF 是否能彻底解决这个问题?
  2. WAIT 1 1000 保证了什么?
  3. RPO 与复制延迟有什么关系?

常见错误回答

“只要开启 AOF everysec,Cluster 就绝不会丢数据。”

评分点

能够区分复制、持久化、客户端确认和故障选举。


问题

9. Cluster 中多 Key、事务和 Lua 有什么限制?

推荐回答

30 秒回答: 一次服务端原子执行涉及的所有 Key 必须属于同一 Hash Slot,包括多 Key 命令、事务、WATCH 和 Lua。通常通过 Hash Tag 将同一业务实体的 Key 放入同槽。

普通 Pipeline 可以被客户端拆分到多个节点,但它只是网络优化,不能提供跨节点原子性。(Redis)

面试官追问

  1. 为什么普通 Pipeline 可以跨槽?
  2. TxPipeline 为什么不能跨槽?
  3. 如何设计用户余额和流水 Key?

常见错误回答

“使用 Pipeline 后,多节点命令就自动成为事务。”

评分点

区分命令原子性、事务、Lua、Pipeline 和分布式事务。


问题

10. Redis Cluster 如何在线扩容?

推荐回答

30 秒回答: 先将空节点加入集群,再从已有主节点迁移部分槽到新节点。经典流程将目标槽设为 IMPORTING、源槽设为 MIGRATING,逐批 MIGRATE Key,最后通过 SETSLOT NODE 切换槽位所有权。

客户端在迁移期间通过 ASK 访问已经迁走的 Key。Redis 8.4+ 还提供 CLUSTER MIGRATION 原子槽迁移任务。(Redis)

面试官追问

  1. 为什么新节点加入后不会自动分担数据?
  2. 迁槽过程中请求为什么还能继续?
  3. BigKey 对扩容有什么影响?

常见错误回答

“把新节点加进 Cluster 后,Redis 会立即自动平均所有数据。”

评分点

覆盖加节点、迁槽状态、ASK、所有权切换、检查和 BigKey 风险。


问题

11. 如何发现和处理热点槽?

推荐回答

30 秒回答: 不能只比较每个节点的槽数,应同时观察槽内 Key 数、内存、CPU、网络和访问频率。热点通常来自过粗 Hash Tag、HotKey、BigKey、超大租户或全局单 Key。

Redis 8.2+ 可使用 CLUSTER SLOT-STATS;旧版本可以结合节点指标、命令统计、HotKey 分析和 CLUSTER KEYSLOT 抽样。(Redis)

面试官追问

  1. 迁移热点槽一定能解决 HotKey 吗?
  2. 单个全局排行榜如何拆分?
  3. 槽位数量均衡为什么可能没有意义?

常见错误回答

“每个主节点分配相同数量的槽就一定均衡。”

评分点

能区分槽热点、Key 热点、内存倾斜和业务模型问题。


问题

12. go-redis 的 ClusterClient 如何处理路由和批量请求?

推荐回答

30 秒回答: redis.NewClusterClient 使用 Seed 节点建立槽位拓扑,根据 Key 计算槽位并访问目标节点,内部处理 MOVEDASK 和有限重试。客户端可被多个 Goroutine 复用,连接池按节点建立。

普通 Pipeline 可以按目标节点拆分并并发执行,因此可能部分成功;TxPipeline 使用 MULTI/EXEC,带 Key 的命令必须同槽。业务不能把跨槽 Pipeline 当成分布式事务。(Redis)

面试官追问

  1. PoolSize 是整个集群还是每个节点?
  2. 网络超时后为什么不能总是安全重试 INCR
  3. 什么时候可以开启 RouteByLatency

常见错误回答

“ClusterClient 只维护一条连接,并把所有请求发送给 Seed 节点。”

评分点

覆盖槽位路由、重定向、并发安全、每节点连接池、Pipeline 部分成功和幂等性。


11. 一分钟面试回答

Redis Cluster 同时提供数据分片和分片级高可用。它把 Key 通过 CRC16 mod 16384 映射到固定槽,再把槽分配给主节点;节点变化时迁移的是槽位及其 Key,而不是改变 Key 的槽号。客户端缓存槽到节点的映射,MOVED 表示槽位永久变化,ASK 表示迁槽期间只临时重定向当前命令。节点通过 Cluster Bus 和 Gossip 传播拓扑,超时后先形成本地 PFAIL,多数主节点确认后形成 FAIL,副本获得多数票并用更高 configEpoch 接管槽位。Cluster 使用异步复制,因此不是强一致系统,已经返回 OK 的写入仍可能在故障转移中丢失。多 Key 命令、事务和 Lua 必须同槽,通常使用 Hash Tag。扩容本质是加入空节点并迁移槽;性能治理不能只看槽数,还要看热点 Key、内存、CPU和网络。


12. 本章总结

  1. Redis Cluster 的核心映射是 Key → Slot → Master
  2. 16384 个固定槽为路由、故障转移和在线迁移提供了稳定中间层。
  3. Hash Tag 能实现同槽操作,但过度使用会造成热点槽。
  4. MOVED 表示永久拓扑变化,ASK 表示迁槽期间的单次临时重定向。
  5. PFAIL 是本地怀疑,FAIL 是多数主节点报告后的故障确认。
  6. 故障转移依赖副本新鲜度、多数投票和更高 configEpoch
  7. 多数主节点不可达时,Cluster 通常无法继续安全选举并进入失败状态。
  8. 异步复制使 Redis Cluster 具有非零 RPO,WAIT 只能降低风险。
  9. 多 Key、事务和 Lua 必须同槽;跨槽 Pipeline 不具备原子性。
  10. 扩容和缩容的本质是槽位迁移,迁移成本取决于实际 Key 和字节数。
  11. 槽数均衡不等于负载均衡,必须治理 HotKey、BigKey 和不合理 Hash Tag。
  12. go-redis ClusterClient 应全局复用,并对超时、重试、部分成功和副本旧读进行业务级控制。

13. 自测清单

  1. user:{100}:profileorder:{100}:detail 是否一定同槽?为什么?
  2. 为什么收到 ASK 后不能永久修改本地槽位表?
  3. PFAIL 为什么不足以直接触发副本晋升?
  4. 三主三从中,两个主节点所在故障域同时完全离线时会发生什么?
  5. 为什么 WAIT 不能把 Redis Cluster 变成严格强一致系统?
  6. 普通 Pipeline 跨三个主节点执行时,可能出现哪些部分成功状态?
  7. 一个节点槽数最少,为什么它的内存仍可能最高?
  8. 缩容删除主节点前必须完成哪些步骤?
  9. RouteByLatency 对 Read-After-Write 语义有什么影响?
  10. 如何为超大租户设计 Hash Tag,既支持局部原子操作,又避免单槽热点?

14. 官方资料

  • Redis Cluster Specification。(Redis)
  • Scale with Redis Cluster。(Redis)
  • CLUSTER SHARDS 命令文档。(Redis)
  • CLUSTER SLOT-STATS 命令文档。(Redis)
  • CLUSTER MIGRATION 命令文档。(Redis)
  • Redis Go 客户端集群连接文档。(Redis)
  • go-redis osscluster.go 官方源码。(GitHub)